/**
*    Copyright 2013 Matthieu Moquet
*    Copyright 2016-2017 LasLabs Inc.
*    Version 2.0.1
*    License MIT (https://opensource.org/licenses/MIT)
**/

(function() {
    'use strict';

    window.Darkroom = Darkroom;

    // Core object of DarkroomJS.
    // Basically it's a single object, instanciable via an element
    // (it could be a CSS selector or a DOM element), some custom options,
    // and a list of plugin objects (or none to use default ones).
    function Darkroom(element, options, plugins) {
        return this.constructor(element, options, plugins);
    }

    // Create an empty list of plugin objects, which will be filled by
    // other plugin scripts. This is the default plugin list if none is
    // specified in Darkroom's constructor.
    Darkroom.plugins = [];

    Darkroom.prototype = {
        // Reference to the main container element
        containerElement: null,

        // Reference to the Fabric canvas object
        canvas: null,

        // Reference to the Fabric image object
        image: null,

        // Reference to the Fabric source canvas object
        sourceCanvas: null,

        // Reference to the Fabric source image object
        sourceImage: null,

        // Track of the original image element
        originalImageElement: null,

        // Stack of transformations to apply to the image source
        transformations: [],

        // Default options
        defaults: {
            // Canvas properties (dimension, ratio, color)
            minWidth: null,
            minHeight: null,
            maxWidth: null,
            maxHeight: null,
            ratio: null,
            backgroundColor: '#fff',

            // Plugins options
            plugins: {},

            // Post-initialisation callback
            initialize: function() { /* noop */ }
        },

        // List of the instancied plugins
        plugins: {},

        // This options are a merge between `defaults` and the options passed
        // through the constructor
        options: {},

        constructor: function(element, options) {
            this.options = Darkroom.Utils.extend(options, this.defaults);

            if (typeof element === 'string')
                element = document.querySelector(element);
            if (null === element)
                return;

            var image = new Image();
            var parent = element.parentElement;
            image.onload = function() {
                // Initialize the DOM/Fabric elements
                this._initializeDOM(element, parent);
                this._initializeImage();

                // Then initialize the plugins
                this._initializePlugins(Darkroom.plugins);

                // Public method to adjust image according to the canvas
                this.refresh(function() {
                    // Execute a custom callback after initialization
                    this.options.initialize.bind(this).call();
                }.bind(this));
            }.bind(this);

            image.src = element.src;
        },

        selfDestroy: function() {
            var container = this.containerElement;
            var image = new Image();
            image.onload = function() {
                container.parentNode.replaceChild(image, container);
            };

            image.src = this.sourceImage.toDataURL();
        },

        // Add ability to attach event listener on the core object.
        // It uses the canvas element to process events.
        addEventListener: function(eventName, callback) {
            var el = this.canvas.getElement();
            if (el.addEventListener) {
                el.addEventListener(eventName, callback);
            } else if (el.attachEvent) {
                el.attachEvent('on' + eventName, callback);
            }
        },

        dispatchEvent: function(eventName) {
            // Use the old way of creating event to be IE compatible
            // See https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
            var event = document.createEvent('Event');
            event.initEvent(eventName, true, true);

            this.canvas.getElement().dispatchEvent(event);
        },

        // Adjust image & canvas dimension according to min/max width/height
        // and ratio specified in the options.
        // This method should be called after each image transformation.
        refresh: function(next) {
            var clone = new Image();
            clone.onload = function() {
                this._replaceCurrentImage(new fabric.Image(clone));
                if (next) next();
            }.bind(this);
            clone.src = this.sourceImage.toDataURL();
        },

        _replaceCurrentImage: function(newImage) {
            if (this.image) {
                this.image.remove();
            }

            this.image = newImage;
            this.image.selectable = false;

            // Adjust width or height according to specified ratio
            var viewport = Darkroom.Utils.computeImageViewPort(this.image);
            var canvasWidth = viewport.width;
            var canvasHeight = viewport.height;

            if (null !== this.options.ratio) {
                var canvasRatio = +this.options.ratio;
                var currentRatio = canvasWidth / canvasHeight;

                if (currentRatio > canvasRatio) {
                    canvasHeight = canvasWidth / canvasRatio;
                } else if (currentRatio < canvasRatio) {
                    canvasWidth = canvasHeight * canvasRatio;
                }
            }

            // Then scale the image to fit into dimension limits
            var scaleMin = 1;
            var scaleMax = 1;
            var scaleX = 1;
            var scaleY = 1;

            if (null !== this.options.maxWidth && this.options.maxWidth < canvasWidth) {
                scaleX =  this.options.maxWidth / canvasWidth;
            }
            if (null !== this.options.maxHeight && this.options.maxHeight < canvasHeight) {
                scaleY =  this.options.maxHeight / canvasHeight;
            }
            scaleMin = Math.min(scaleX, scaleY);

            scaleX = 1;
            scaleY = 1;
            if (null !== this.options.minWidth && this.options.minWidth > canvasWidth) {
                scaleX =  this.options.minWidth / canvasWidth;
            }
            if (null !== this.options.minHeight && this.options.minHeight > canvasHeight) {
                scaleY =  this.options.minHeight / canvasHeight;
            }
            scaleMax = Math.max(scaleX, scaleY);

            var scale = scaleMax * scaleMin; // one should be equals to 1

            canvasWidth *= scale;
            canvasHeight *= scale;

            // Finally place the image in the center of the canvas
            this.image.setScaleX(1 * scale);
            this.image.setScaleY(1 * scale);
            this.canvas.add(this.image);
            this.canvas.setWidth(canvasWidth);
            this.canvas.setHeight(canvasHeight);
            this.canvas.centerObject(this.image);
            this.image.setCoords();
        },

        // Apply the transformation on the current image and save it in the
        // transformations stack (in order to reconstitute the previous states
        // of the image).
        applyTransformation: function(transformation) {
            this.transformations.push(transformation);

            transformation.applyTransformation(
                this.sourceCanvas,
                this.sourceImage,
                this._postTransformation.bind(this)
            );
        },

        _postTransformation: function(newImage) {
            if (newImage)
                this.sourceImage = newImage;

            this.refresh(function() {
                this.dispatchEvent('core:transformation');
            }.bind(this));
        },

        // Initialize image from original element plus re-apply every
        // transformations.
        reinitializeImage: function() {
            this.sourceImage.remove();
            this._initializeImage();
            this._popTransformation(this.transformations.slice());
        },

        _popTransformation: function(transformations) {
            if (0 === transformations.length) {
                this.dispatchEvent('core:reinitialized');
                this.refresh();
                return;
            }

            var transformation = transformations.shift();

            var next = function(newImage) {
                if (newImage) this.sourceImage = newImage;
                this._popTransformation(transformations);
            };

            transformation.applyTransformation(
                this.sourceCanvas,
                this.sourceImage,
                next.bind(this)
            );
        },

        // Create the DOM elements and instanciate the Fabric canvas.
        // The image element is replaced by a new `div` element.
        // However the original image is re-injected in order to keep a trace of it.
        _initializeDOM: function(imageElement) {
            // Container
            var mainContainerElement = document.createElement('div');
            mainContainerElement.className = 'darkroom-container';

            // Toolbar
            var toolbarElement = document.createElement('div');
            toolbarElement.className = 'darkroom-toolbar';
            mainContainerElement.appendChild(toolbarElement);

            // Viewport canvas
            var canvasContainerElement = document.createElement('div');
            canvasContainerElement.className = 'darkroom-image-container';
            var canvasElement = document.createElement('canvas');
            canvasContainerElement.appendChild(canvasElement);
            mainContainerElement.appendChild(canvasContainerElement);

            // Source canvas
            var sourceCanvasContainerElement = document.createElement('div');
            sourceCanvasContainerElement.className = 'darkroom-source-container';
            sourceCanvasContainerElement.style.display = 'none';
            var sourceCanvasElement = document.createElement('canvas');
            sourceCanvasContainerElement.appendChild(sourceCanvasElement);
            mainContainerElement.appendChild(sourceCanvasContainerElement);

            // Original image
            imageElement.parentNode.replaceChild(mainContainerElement, imageElement);
            imageElement.style.display = 'none';
            mainContainerElement.appendChild(imageElement);

            // Instanciate object from elements
            this.containerElement = mainContainerElement;
            this.originalImageElement = imageElement;

            this.toolbar = new Darkroom.UI.Toolbar(toolbarElement);

            this.canvas = new fabric.Canvas(canvasElement, {
                selection: false,
                backgroundColor: this.options.backgroundColor,
            });

            this.sourceCanvas = new fabric.Canvas(sourceCanvasElement, {
                selection: false,
                backgroundColor: this.options.backgroundColor,
            });
        },

        // Instanciate the Fabric image object.
        // The image is created as a static element with no control,
        // then it is add in the Fabric canvas object.
        _initializeImage: function() {
            this.sourceImage = new fabric.Image(this.originalImageElement, {
                // Some options to make the image static
                selectable: false,
                evented: false,
                lockMovementX: true,
                lockMovementY: true,
                lockRotation: true,
                lockScalingX: true,
                lockScalingY: true,
                lockUniScaling: true,
                hasControls: false,
                hasBorders: false,
            });

            this.sourceCanvas.add(this.sourceImage);

            // Adjust width or height according to specified ratio
            var viewport = Darkroom.Utils.computeImageViewPort(this.sourceImage);
            var canvasWidth = viewport.width;
            var canvasHeight = viewport.height;

            this.sourceCanvas.setWidth(canvasWidth);
            this.sourceCanvas.setHeight(canvasHeight);
            this.sourceCanvas.centerObject(this.sourceImage);
            this.sourceImage.setCoords();
        },

        // Initialize every plugins.
        // Note that plugins are instanciated in the same order than they
        // are declared in the parameter object.
        _initializePlugins: function(plugins) {
            for (var name in plugins) {
                var plugin = plugins[name];
                var options = this.options.plugins[name];

                // Setting false into the plugin options will disable the plugin
                if (options === false)
                    continue;

                // Avoid any issues with _proto_
                if (!plugins.hasOwnProperty(name))
                    continue;

                this.plugins[name] = new plugin(this, options);
            }
        },
    };
})();
